package org.apogeescience.query;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import org.apogeescience.annotation.Column;
import org.apogeescience.annotation.NamedQueries;
import org.apogeescience.annotation.NamedQuery;
import org.apogeescience.annotation.Table;
import org.apogeescience.connector.DatabaseDriver;
import org.apogeescience.exceptions.TableNotDefinedException;
import org.apogeescience.log.LogFactory;
import org.apogeescience.log.Logger;
import org.apogeescience.test.User;

/**
 * Query class executes all queries
 * @author Isaac
 *
 */
public class Query {

	private Hashtable<String, Object> parameters = new Hashtable<String, Object>();

	private Class<?> queryClass;

	private NamedQuery namedQuery;

	private String tableName;

	private Logger logger = LogFactory.getLogger(Query.class);

	/**
	 * Default constructor
	 */
	public Query() {	}

	/**
	 * Default constructor
	 * @param namedQuery named query to get
	 * @param clazz the class to query
	 */
	public Query(String namedQuery, Class<?> clazz) {
		try {
			getNamedQuery(namedQuery, clazz);
		} catch (TableNotDefinedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Get a specified named query
	 * @param namedQuery Named Query to retrieve
	 * @param clazz class to query
	 * @throws TableNotDefinedException
	 */
	public void getNamedQuery(String namedQuery, Class<?> clazz) throws TableNotDefinedException {
		if (!clazz.isAnnotationPresent(Table.class)) {
			throw new TableNotDefinedException();
		}

		this.queryClass = clazz;

		NamedQueries namedQueries = this.queryClass.getAnnotation(NamedQueries.class);
		for(NamedQuery nq : namedQueries.value()) {
			if (nq.name().equals(namedQuery)) {
				this.namedQuery = nq;
				break;
			}
		}

		Table table = this.queryClass.getAnnotation(Table.class);

		if (table != null && !table.name().isEmpty()) {
			this.tableName = table.name();
		} else {
			this.tableName = this.queryClass.getSimpleName();
		}

		if (table != null && !table.tablespace().isEmpty()) {
			this.tableName = String.format("%s.%s", table.tablespace(), this.tableName);
		}
	}

	/**
	 * Sets parameters for the named query
	 * @param parameter parameter to add
	 * @param value value of the parameter
	 */
	public void setParameter(String parameter, Object value) {
		if (this.parameters.containsKey(parameter)) {
			this.parameters.remove(parameter);
		}
		this.parameters.put(parameter, value);
	}

	/**
	 * Execute the named query
	 * @return
	 */
	public List<?> execute() {
		String query = DatabaseDriver.generateQuery(this.tableName, this.namedQuery, this.parameters);
		return executeNativeQuery(query);
	}

	/**
	 * Execute a native query
	 * @param query Query to execute
	 * @return
	 */
	public List<?> executeNativeQuery(String query) {
		this.logger.info("Executing Query: " + query);

		List<Object> results = new ArrayList<>();

		try {
			Map<String, List<Object>> set = DatabaseDriver.executeQuery(query);

			Object newObject = null;

			do {
				Class<?> cls = Class.forName(this.queryClass.getName());
				newObject = cls.newInstance();

				for (String columnName : set.keySet()) {
					List<Object> object = set.get(columnName);
					if (object.isEmpty()) {
						newObject = null;
						break;
					}
					Object value = object.remove(0);

					Method[] methods = cls.getMethods();

					for (Method method : methods) {
						Column elem = method.getAnnotation(Column.class);
						if (elem == null || !elem.name().equalsIgnoreCase(columnName)) continue;

						for (Method method2: methods) {
							if (method2.getName().startsWith("set") && 
									method2.getName().toLowerCase().contains(columnName.toLowerCase())) {
								method2.invoke(newObject, new Object[] {value});
								break;
							}
						}
					}
				}

				if (newObject != null) {
					results.add(newObject);
				}
			} while(newObject != null);

		} catch (Exception e) {
			this.logger.error("Unable to execute query", e);
		}

		return results;
	}

	/**
	 * Checks to see if the named query was found
	 * @return
	 */
	public boolean namedQueryFound() {
		return this.namedQuery != null;
	}

	/**
	 * Checks to see if the parameters were set
	 * @return
	 */
	public boolean hasParameters() {
		return this.parameters.size() > 0;
	}

	public void persist(Class<?> clazz) {
		// TODO Auto-generated method stub
		
	}
}
